1 מרכז ההדרכה 2000 תמיכה ועדכונים עדכון מס' 48 מאי 2002 מימוש מכונת מצבים (FSM) באמצעות State Pattern מבוא מכונת מצבים סופית Machine) (Final State היא מודל מקובל בניתוח מערכות באופן כללי, ומערכות חומרה ותוכנה בפרט. במודל זה, ליישות מסויימת נקבעים מספר מצבים בהם היא יכולה להיות, והמעברים בין המצבים מתבצעים כתלות בקלט כלשהו. למעשה, ניתן לתאר כל עצם תוכנה כמכונת מצבים: קבוצת הנתונים שהוא כולל מגדירה את המצב בו הוא נמצא - כלומר, ערכיהם ברגע מסויים מתאר את מצבו באותו רגע. השתנות ערכים אלו כתלות בקלט או באירועים מסויימים מהווה מעבר מצב. במאמר זה נכיר Pattern ידוע לייצוג מכונות מצבים הנקרא,State ונעמוד על יתרונותיו ביחס ללוגיקת משפטי תנאי switch) (if / תוך שימוש בפולימורפיזם מתקדם. להרחבה בנושא State ו- Design Patterns עיין/י בספר "++C - מדריך מקצועי" בהוצאת "מרכז ההדרכה 2000". State Pattern בעיה נדרש לבנות תוכנה עבור מכשיר טלפון סלולרי. בטלפון זה שלושה מקשים עיקריים, מלבד מקשי הספרות/אותיות: call exit menu
2 המכשיר הסלולרי ייוצג ע"י המחלקה,CellPhone שתכלול את השירותים הבאים: CellPhone +call() +showlastcall() +showmenu() +exitmenu() +answer() +disconnect() הסבר: שלוש הפונקציות הראשונות הן פונקציות התגובה ללחיצות על שלושת הכפתורים המתאימים במכשיר: "menu" נלחץ המקש - menubutton() "call" נלחץ המקש - callbutton() "exit" נלחץ המקש - exitbutton() שאר הפונקציות: call() - ביצוע התקשרות showlastcall() - הצגת המספר האחרון שאליו התקשרנו מהמכשיר showmenu() - הצגת התפריט exitmenu() - יציאה מהתפריט answer() - מענה לשיחה (בזמן צלצול) disconnect() - ניתוק השיחה. להלן תרשים המצבים של המערכת: "exit" "menu" "call" Idle "exit" Menu "call" "exit" "exit" Dialing triggerless Talking "call" Ringing לחיצה על "menu" מציגה את התפריט. במידה והתפריט כבר מוצג לא מבוצע דבר. בזמן שיחה, צלצול וחיוג לא מבוצע דבר. לחיצה על "call" גורמת לחיוג למספר המוצג על המסך (קריאה ל-.(call() במידה ולא מוצג מספר כשלהו, לחיצה זו תגרום להצגת המספר האחרון אליו חייגנו (קריאה ל- showlastcall() ). במידה
3 והמכשיר מצלצל, לחיצה על כפתור זה תגרום למענה לשיחה (קריאה ל-.(answer() לחיצה על "exit" גורמת לניתוק השיחה הנוכחית (קריאה ל- (disconnect() במידה ואנו באמצע שיחה, להפסקת הצלצול במידה והמכשיר מצלצל, או להפסקת החיוג כאשר המכשיר במצב חיוג. במצב של הצגת התפריט, לחיצה על "exit" גורמת ליציאה מהתפריט (קריאה ל-.(exitMenu() הערה : המימוש כאן הוא פשטני ואינו כולל את הטיפול המלא בכלל השירותים של מכשיר סלולרי מודרני. פתרון שגוי נגדיר enum של אוסף המצבים האפשריים של המכשיר, ונבצע משפט switch-case בכל פונקצית תגובה לבדיקת המצב הנוכחי. כך מוכרזת המחלקה :CellPhone class CellPhone enum State IDLE, DIALING, RINGING, TALKING, MENU; State m_state; // Buttons input response methods void menubutton(); void callbutton(); void exitbutton(); ; // cellular methods void call(); void showlastcall(); void showmenu(); void exitmenu(); void answer() ; void disconnect(); void CellPhone::menuButton() switch(m_state) case IDLE: m_state = MENU; showmenu(); case DIALING: case RINGING: case TALKING: case MENU: void CellPhone::callButton() // do nothing פונקצית התגובה ללחיצה על :"menu" פונקצית התגובה ללחיצה על :"call"
4 switch(m_state) case IDLE: m_state = MENU; showlastcall(); case RINGING: m_state = TALKING; answer(); case MENU: m_state = DIALING; call(); m_state = TALKING; case DIALING: case TALKING: void CellPhone::exitButton() switch(m_state) case RINGING: case DIALING: case TALKING: disconnect(); m_state = IDLE; case MENU: exitmenu(); m_state = IDLE; // do nothing ופונקצית התגובה ללחיצה על :"exit" case IDLE: // do nothing חסרונות הפתרון: יעילות: חיפוש המצב הנוכחי במשפט switch-case אינו יעיל, מכיוון שבאופן ממוצע, עוברים בכל קריאה לפונקציה כנ"ל על מחצית מספר המצבים. כמו כן משפט switch-case אינו שומר את המידע שנרכש (מציאת המצב המתאים) עבור הפונקציות הבאות, גם כאשר המצב לא שונה. מודולריות: קוד המחלקה CellPhone אינו מודולרי. הטיפול בכל המצבים מבוצע באופן ריכוזי במשפט switch-case במחלקה, ומחייב שינויים במספר מקומות בכל שינוי או הוספה בדרישות היישום.
5 פתרון: State Pattern רעיון הפתרון הוא להשתמש בפולימורפיזם כתחליף למשפט :switch-case נגדיר מחלקת מצב אבסטרקטית,,State ומחלקות נורשות ממנה המייצגות את מצבי המערכת השונים: m_currentstate->menubutton() m_currentstate->callbutton() CellPhone -vector<state*> m_states -m_currentstate +call() +showlastcall() +showmenu() +exitmenu() +answer() +disconnect() +setstate() m_states m_currentstate m_currentstate->exitbutton() 5 «interface» State Idle Dialing Ringing Talking Menu המחלקה CellPhone מחזיקה וקטור של מצביעים ל- (m_states) State ומצביע נוסף ל- State הנוכחי.(m_currentState).CellPhone מחזיקה גם היא מצביע למחלקה State לכל מצב מוגדרת מחלקה מתאימה הנורשת מ- State ומממשת את שלוש פונקציות התגובה ללחיצה על המקשים. כאשר נלחץ המקש "menu" לדוגמא, מופעלת הפונקציה,CellPhone::menuButton() וזו מצבעת הפנייה (Forwarding) לפונקציה,m_currentState->menuButton() כלומר, לפונקציה של המצב הנוכחי. הפונקציות המפנות במחלקה CellPhone הן,inline ולכן אין פה אובדן יעילות בביצוע קריאות עקיפות לפונקציות של.State כך מוכרז הממשק :State class State virtual void menubutton(cellphone *cellphone) = 0; virtual void callbutton(cellphone *cellphone) = 0; virtual void exitbutton(cellphone *cellphone) = 0; ; וכך למשל מוגדרת המחלקה Idle היורשת מ- State ומייצג מצב המתנה: class Idle : public State void menubutton(cellphone *cellphone) cellphone->showmenu(); cellphone->setstate(menu); void callbutton(cellphone *cellphone) cellphone->showlastcall();
6 ; cellphone->setstate(menu); void exitbutton(cellphone *cellphone) הסבר: Idle מממשת את הפונקציות הוירטואליות הטהורות שהוגדרו ב-,State בהתאם למוגדר עבור מצב.IDLE המחלקה הראשית,,CellPhone מבצע הפנייה (forwarding) של קריאות לפונקציות מצב למחלקת המצב הנוכחי: class CellPhone enum State_enum IDLE, DIALING, RINGING, TALKING, MENU, STATE_SIZE; CellPhone() : m_states(state_size) m_states.push_back(new Idle); m_states.push_back(new Dialing); m_states.push_back(new Ringing); m_states.push_back(new Talking); m_states.push_back(new Menu); m_currentstate = m_states[idle]; void setstate(state_enum s) m_currentstate = m_states[s]; // Buttons input response methods void menubutton() m_currentstate->menubutton(this); void callbutton() m_currentstate->callbutton(this); void exitbutton() m_currentstate->exitbutton(this); // cellular methods void call(); void showlastcall(); void showmenu(); void exitmenu(); void answer() ; void disconnect(); private: State* vector<state*> ; m_currentstate; m_states;
7 שיפור נוסף : קינון מחלקות והסתרת מידע מכיוון שהמשתמש במחלקה אינו אמור לעשות שימוש במחלקות המצב, כדאי להגדירן כמקוננות private בתוך המחלקה.CellPhone הקוד המלא של המחלקה נראה אם כן כעת כך: #include <vector> using namespace std; class CellPhone enum State_enum IDLE, DIALING, RINGING, TALKING, MENU, STATE_SIZE; CellPhone() : m_states(state_size) m_states.push_back(new Idle); m_states.push_back(new Dialing); m_states.push_back(new Ringing); m_states.push_back(new Talking); m_states.push_back(new Menu); m_currentstate = m_states[idle]; void setstate(state_enum s) m_currentstate = m_states[s]; // Buttons input response methods void menubutton() m_currentstate->menubutton(this); void callbutton() m_currentstate->callbutton(this); void exitbutton() m_currentstate->exitbutton(this); // cellular methods void call(); void showlastcall(); void showmenu(); void exitmenu(); void answer() ; void disconnect(); private: // state interface class State virtual void menubutton(cellphone *cellphone) = 0; virtual void callbutton(cellphone *cellphone) = 0; virtual void exitbutton(cellphone *cellphone) = 0; ; class Idle : public State void menubutton(cellphone *cellphone) cellphone->showmenu(); cellphone->setstate(menu);
8 ; void callbutton(cellphone *cellphone) cellphone->showlastcall(); cellphone->setstate(menu); void exitbutton(cellphone *cellphone) class Dialing: public State void menubutton(cellphone *cellphone) void callbutton(cellphone *cellphone) void exitbutton(cellphone *cellphone) cellphone->disconnect(); cellphone->setstate(idle); ; class Ringing: public State void menubutton(cellphone *cellphone) void callbutton(cellphone *cellphone) cellphone->answer(); cellphone->setstate(talking); void exitbutton(cellphone *cellphone) cellphone->disconnect(); cellphone->setstate(idle); ; class Talking: public State void menubutton(cellphone *cellphone) void callbutton(cellphone *cellphone) void exitbutton(cellphone *cellphone) cellphone->disconnect(); cellphone->setstate(idle); ; class Menu : public State void menubutton(cellphone *cellphone) void callbutton(cellphone *cellphone) cellphone->call(); cellphone->setstate(dialing); void exitbutton(cellphone *cellphone) cellphone->disconnect(); cellphone->setstate(idle);
9 ; ; State* vector<state*> m_currentstate; m_states; כפי שניתן לראות, פתרון זה עדיף משתי הבחינות שהוזכרו על פני הפתרון הקודם: יעילות: המצב הנוכחי נשמר, ולא מבוצע חיפוש כלשהו. במלים אחרות, לעומת סיבוכיות חיפוש המצב בפתרון הקודם,O(n) הסיבוכיות כאן היא (1)O. מודולריות: הפתרון מודולרי יותר עקב המבנה הפולימורפי. הוספה של מצב חדש מתבטאת בהוספת מחלקה חדשה, ובהוספת עצם ממנה באתחול. כמו כן שינוי בהתנהגות במצב מסוים משפיעה אך ורק על הקוד של מחלקת המצב המתאימה.
10 הכללה State הוא Pattern המשמש להגדרת מכונת מצבים: Context +func1() +func2() m_states m_currentstate «interface» State +func1() +func2() StateA StateB StateC +func1() +func2() +func1() +func2() +func1() +func2() לכל מצב של מחלקה נתונה (context) מגדירים מחלקה היורשת מהממשק State ומממשת את הפונקציות המוכרזות בו. פונקציות אלו מאופיינות בכך שביצוען תלוי במצב של.Context אפשרויות ווריאציות: ה- Context יכול ליצור את וקטור המצבים האפשריים ולהקצות אותם מראש, או באופן הדרגתי, לייצר עצם בהגעה למצב המתאים. שינוי מצב של המערכת: יכול להתבצע ע"י ה-,Context ע"י ה- States או ע"י שניהם. סיכום מימוש מכונת מצבים (FSM) במערכות מונחות עצמים ניתן לביצוע ע"י ה-.State Pattern למימוש זה יתרונות בולטים על פני המימוש המסורתי ע"י משפט :Switch הוא יעיל יותר הוא מודולרי יותר כאשר מספר המצבים רב ו/או קיימת היררכייה של מכונות מצבים (תת מכונות מצבים), המימוש ע"י State Pattern מסורבל ומורכב. במצב זה, פתרון פשוט יותר הוא מימוש ע"י טבלת מעברי מצב. כל הזכויות שמורות מאיר סלע מרכז ההדרכה 2000